[Generic表]TDRBlob内嵌Protobuf部分字段读写

[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写

混合字段路径的综合读写示例、路径语法及限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 C++ 的 TdrPbFieldGroup、SetAllTdrPbFieldNames,以及完整的 Service API 请求与响应示例。

1. 接口说明

TDR 表的 Blob 字段(char 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。

三个命令对应三个命令字:

命令命令字值说明
FieldGetTCAPLUS_API_PB_FIELD_GET_REQ0x0067读取 Blob 中指定的 PB 字段
FieldSetTCAPLUS_API_PB_FIELD_SET_REQ0x0069更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入
BatchFieldGetTCAPLUS_API_PB_BATCH_FIELD_GET_REQ0x0075多个主键共用一组字段路径批量读取

Blob 中 Protobuf 字段的自增(TCAPLUS_API_PB_FIELD_INCREASE_REQ)在 TDR 表上不支持。

本特性的字段名适配层是可选头文件 tcaplus_tdr_pb_fields.h,只替换这一行:

request->SetFieldNames(field_names, field_count);

其余请求和响应流程保持 Service API 原有用法不变。

不 include 该头文件的 TDR 用户不会被强制依赖 protobuf。

2. 版本要求

  • Service API 3.55.0 SP06(3.55.0.209070)及以上;
  • 服务端须为 Tcapsvr 3.73.0 2026-10-10 以后的版本。旧版后端在这三个命令上返回 SVR_ERR_FAIL_TO_FIND_META,需要升级后端。

3. 准备工作

参见准备工作文档,完成使用该接口前的准备工作,并创建 TDR Generic 表。

本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段。下面以 RoleSummaryTable 为例:

<struct name="Col" version="1">
    <entry name="len"   type="uint" defaultvalue="0" desc="bin 的有效长度" />
    <entry name="bin"   type="char" count="1024" refer="len" desc="PB 数据" />
</struct>

<struct name="RoleSummaryTable" version="1" primarykey="id" splittablekey="id">
    <entry name="id"    type="int64" desc="角色ID" />
    <entry name="del"   type="int"   desc="删除标记" />
    <entry name="col1"  type="Col"   desc="保存 ProfileBaseData 的 Blob" />
</struct>

col1.bin 是保存 PB 的 Blob 字段,col1.len 是它的 refer 长度字段。

注意:字段路径里用 TDR 表定义的字段名(col1.bin、col1.len、del)。

Blob 里的 PB 定义示例如下(ProfileBaseData后面都是使用该proto进行示例演示):

syntax = "proto3";
package tcaplus_demo;

message ProfileBaseData {
    uint32 level = 5;
    string plat_name = 6;
    repeated uint32 equipped_tag_list = 16;
    map<int32, BanInfo> ban_info_map = 22;
    map<string, string> settings = 23;
}
message BanInfo { string info = 1; int32 duration = 2; }

该proto表定义不用上传到Tcaplus服务端,只存在于客户端

完整示例见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields:

内容路径
表定义examples/tcaplus/C++_tdr2.0_tdr_pb_fields/table_test.xml
PB 定义examples/tcaplus/C++_tdr2.0_tdr_pb_fields/profile_base_data.proto
示例代码examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp
编译与运行examples/tcaplus/C++_tdr2.0_tdr_pb_fields/readme.txt

示例里的三个函数分别对应下面 5.1、5.2、5.3,响应处理对应 5.4。

4. PB部分字段操作方式

4.1. 方案1

部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径,直接用点分式写PB数字tagid:

// 操作PB ProfileBaseData 的 level字段
// 操作PB ProfileBaseData 的 plat_name字段
// 操作PB ProfileBaseData 的 equipped_tag_list 字段(proto3数组默认 packed,只能整数组读写)
// 操作PB ProfileBaseData 的 ban_info_map 字段的map元素1001
// 操作PB ProfileBaseData 的 settings 字段的map元素region
// 操作tdr原生字段del
const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.16", "col1.bin.22[1001]", "col1.bin.23['region']", "del"};
ret = request->SetFieldNames(field_names, sizeof(field_names) / sizeof(field_names[0]));

4.2. 方案2

直接使用tagid操作字段,不够直观,维护不方便,因此我们增加了PB字段名到 tagid 映射的适配层用法,方便维护。

字段名到 tag 路径的转换用 tcaplus_tdr_pb_fields.h 里的适配层:

#include "tcaplus_tdr_pb_fields.h"

const char* col1_fields[] = {
    "level",               // 操作PB ProfileBaseData 的 level字段
    "plat_name",           // 操作PB ProfileBaseData 的 plat_name字段
    "equipped_tag_list",   // 操作PB ProfileBaseData 的 equipped_tag_list 字段(proto3数组默认 packed,只能整数组读写)
    "ban_info_map[1001]",  // 操作PB ProfileBaseData 的 ban_info_map 字段的map元素1001
    "settings['region']"   // 操作PB ProfileBaseData 的 settings 字段的map元素region    
};

const TcaplusService::TdrPbFieldGroup field_groups[] = {
    // 操作PB字段 ProfileBaseData
    TcaplusService::TdrPbFieldGroup(
        "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),

    // 如果有tdr中有多个Blob存放了PB Message,比如col2.bin,可以用多个 TdrPbFieldGroup
    // TcaplusService::TdrPbFieldGroup(
    //     "col2.bin", xxx::descriptor(), col2_fields),

    // 操作原生 TDR 字段,原样透传
    TcaplusService::TdrPbFieldGroup("del")
};

// 最终调用会转换成方案1的调用
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);

5. 完整示例代码

示例代码见发布包 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp,对应函数为 SendProfileFieldSet、SendProfileFieldGet、SendProfileBatchFieldGet 和 HandleProfileFieldResponse,入口在 main() 中。

5.1 FieldSet(部分更新)

int32_t SendProfileFieldSet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
    const char kTableName[] = "RoleSummaryTable";

    // 1. 创建并初始化原生 Service API request
    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_SET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 2. 业务构造增量 PB message
    tcaplus_demo::ProfileBaseData delta;
    delta.set_level(100);
    delta.set_plat_name("plat_x");
    (*delta.mutable_ban_info_map())[1001].set_info("ban info");

    ROLESUMMARYTABLE row;
    memset(&row, 0, sizeof(row));
    row.llId = role_id; // 主键

    // 3. PB增量message序列化到TDR Blob
    const size_t encoded_size = delta.ByteSizeLong();
    if (encoded_size > sizeof(row.stCol1.szBin))
    {
        return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
    }
    if (!delta.SerializePartialToArray(row.stCol1.szBin, static_cast<int>(encoded_size)))
    {
        return TcapErrCode::API_ERR_PACK_MESSAGE;
    }
    row.stCol1.dwLen = static_cast<uint32_t>(encoded_size);

    // 4. 继续使用原生 record
    TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
    if (record == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }
    ret = record->SetData(&row, sizeof(row));
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    /////////////////////////////////////////////////////////////////////
    // 5. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
    const char* col1_fields[] = {
        "level",
        "plat_name",
        "ban_info_map[1001]"
    };
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 或者使用方案1的tagid方式调用
    // const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.22[1001]"};
    // ret = request->SetFieldNames(field_names, 3);
    ///////////////////////////////////////////////////////////////////

    return server.SendRequest(request);
}

5.2 FieldGet(部分读取,同时读原生 TDR 字段)

int32_t SendProfileFieldGet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
    const char kTableName[] = "RoleSummaryTable";

    // 1. 创建并初始化原生 Service API request
    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_GET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 2. 只填主键,value 字段由服务端按字段路径回填
    ROLESUMMARYTABLE row;
    memset(&row, 0, sizeof(row));
    row.llId = role_id; // 主键

    // 3. 继续使用原生 record
    TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
    if (record == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }
    ret = record->SetData(&row, sizeof(row));
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    /////////////////////////////////////////////////////////////////////
    // 4. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
    const char* col1_fields[] = {
        "level",
        "plat_name",
        "ban_info_map[1001].info",
        "settings['region']"
    };
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),

        // 原生 TDR 字段不经过 PB 解析,原样透传
        TcaplusService::TdrPbFieldGroup("del")
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 或者使用方案1的tagid方式调用
    // const char* field_names[] = {"col1.bin.5", "col1.bin.6", "col1.bin.22[1001].1",
    //                              "col1.bin.23['region']", "del"};
    // ret = request->SetFieldNames(field_names, 5);
    ///////////////////////////////////////////////////////////////////

    return server.SendRequest(request);
}

5.3 BatchFieldGet(批量读取)

int32_t SendProfileBatchFieldGet(
    TcaplusService::TcaplusServer& server,
    const int64_t role_ids[],
    unsigned role_count)
{
    const char kTableName[] = "RoleSummaryTable";

    // 1. 创建并初始化原生 Service API request
    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_BATCH_FIELD_GET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 2. 每条记录只填主键,一次请求最多 1024 条
    if (role_ids == NULL || role_count == 0 || role_count > 1024)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    for (unsigned i = 0; i < role_count; ++i)
    {
        ROLESUMMARYTABLE row;
        memset(&row, 0, sizeof(row));
        row.llId = role_ids[i]; // 主键

        TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
        if (record == NULL)
        {
            return TcapErrCode::API_ERR_PARAMETER_INVALID;
        }

        ret = record->SetData(&row, sizeof(row));
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            return ret;
        }
    }

    /////////////////////////////////////////////////////////////////////
    // 3. 唯一被适配的步骤:所有记录共用同一组字段路径,只设置一次
    const char* col1_fields[] = {"level", "plat_name"};
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 或者使用方案1的tagid方式调用
    // const char* field_names[] = {"col1.bin.5", "col1.bin.6"};
    // ret = request->SetFieldNames(field_names, 2);
    ///////////////////////////////////////////////////////////////////

    return server.SendRequest(request);
}

SetAllTdrPbFieldNames() 最终设置的是 col1.bin.5 和 col1.bin.6。

与 5.2 的 FieldGet 一样只填主键,区别是一次请求带多条;所有记录共用同一组字段路径, SetAllTdrPbFieldNames() 只调用一次,单次最多 1024 条主键。 批量请求里每条记录各自返回结果,某条主键不存在或字段缺失只影响它自己, 处理响应时逐条看 FetchRecord() 的返回值(见 5.4)。

批量读取还需注意:

  • 不支持条件。 在批量读取请求的 record 上调用 SetCondition() 会返回 GEN_ERR_INVALID_ARGUMENTS。
  • 响应可能分包。 响应包超过 256KB 时服务端会分包返回,分包不会切分单条记录,且无法关闭。收到一个分包后,用 TcaplusServiceResponse::HaveMoreResPkgs() 判断是否还有下一个包,返回 1 时继续调用 RecvResponse() 收包,直到所有分包处理完毕;GetRecordCount() 只返回当前分包中的记录数。

5.4 响应处理

响应端不使用适配层,三个命令共用同一份处理逻辑:直接读取 TDR,再解析 Blob 里的部分 PB。

int32_t HandleProfileFieldResponse(TcaplusService::TcaplusServiceResponse* response)
{
    // 1. 先看整个请求的结果
    if (response == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = response->GetResult();
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 2. 逐条取记录:单条失败不影响其它记录,这里只记录第一个错误
    int32_t first_error = TcapErrCode::GEN_ERR_SUC;
    const int record_count = response->GetRecordCount();
    for (int i = 0; i < record_count; ++i)
    {
        // 2.1 取记录,批量请求里这是这一条自己的结果
        const TcaplusService::TcaplusServiceRecord* record = NULL;
        ret = response->FetchRecord(record);
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = ret;
            }
            continue;
        }

        // 2.2 取回 TDR 结构,Blob 里已经是本次的部分 PB
        ROLESUMMARYTABLE row;
        memset(&row, 0, sizeof(row));
        ret = record->GetData(&row, sizeof(row));
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = ret;
            }
            continue;
        }

        // 2.3 长度字段是本次部分 PB 的长度,业务自己校验上界
        if (row.stCol1.dwLen > sizeof(row.stCol1.szBin))
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
            }
            continue;
        }

        // 2.4 只解析本次返回的部分 PB
        tcaplus_demo::ProfileBaseData profile;
        if (!profile.ParsePartialFromArray(row.stCol1.szBin, static_cast<int>(row.stCol1.dwLen)))
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = TcapErrCode::API_ERR_UNPACK_MESSAGE;
            }
            continue;
        }

        // 2.5 profile 只包含本次请求返回的部分字段,不能当作完整记录使用
        ConsumeProfile(row.llId, row.iDel, profile);
    }

    return first_error;
}

响应端不使用适配层,流程和原生 Service API 完全一致,只有 Blob 的解析由业务自己做。有以下几点需要注意:

  • row.stCol1.dwLen 是本次部分 PB 的长度,不是记录里完整 Blob 的长度;
  • 解析出的 profile 只包含本次请求的字段,不能再拿去整段覆盖 Blob;
  • 批量请求里每条记录各自返回结果,逐条处理时以 FetchRecord() 的返回值判断这一条是否成功;
  • 批量读取的响应可能分包,需要对每个分包调用该函数,直到 HaveMoreResPkgs() 不再返回 1(见 5.3);
  • 该函数在 GetResult() 非 0 时直接返回,适用于读取。FieldSet 失败时如需取回 ResultFlag 指定的数据,见 5.6。

5.5 删除 map 元素

删除 map 中指定 key 的元素时,在字段名前加 POP 和空格。POP 专用于删除 map 的指定 key。请求按 5.1 的方式构造,区别是不需要增量 PB:只填主键,Blob 留空,长度字段填 0。

ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id;   // 主键
row.stCol1.dwLen = 0; // 删除不需要增量 PB

// ... Init(TCAPLUS_API_PB_FIELD_SET_REQ)、AddRecord()、SetData() 同 5.1 ...

const char* col1_fields[] = {"POP ban_info_map[1001]"};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
    TcaplusService::TdrPbFieldGroup(
        "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);

// 或者使用方案1的tagid方式调用
// const char* field_names[] = {"POP col1.bin.22[1001]"};
// ret = request->SetFieldNames(field_names, 1);

删除后 ban_info_map 中其他 key 保留;删除不存在的 key 也返回成功。同一个 key 不能在一个请求里既更新又删除。

5.6 条件与 ResultFlag

条件。 FieldGet 和 FieldSet 在 AddRecord() 返回的 record 上调用 SetCondition(),放在 SetData() 和设置字段路径之后。条件中的 PB 路径使用 tagid,适配层不转换条件文本。BatchFieldGet 不支持条件(见 5.3)。

ret = record->SetCondition("del = 0");
if (ret != TcapErrCode::GEN_ERR_SUC)
{
    return ret;
}

FieldSet 条件不成立时返回 COMMON_ERR_CONDITION_NOT_MATCHED,记录和版本都不变。

ResultFlag。 FieldSet 使用 SetResultFlagForSuccess() 和 SetResultFlagForFail() 分别控制成功、失败时的返回数据,取值含义见《TDR Blob 内嵌 Protobuf 部分字段读写》4.5 节。C++ 需要注意:

  • 两侧都不设置时,成功响应会回带请求中的字段,即整段增量 Blob。只关心执行结果时,建议设置 SetResultFlagForSuccess(0)。
  • 失败响应只有在显式调用 SetResultFlagForFail() 时才能取到记录。此时即使 GetResult() 非 0,只要 GetRecordCount() 大于 0,就可以 FetchRecord() 后 GetData()。未显式设置时,失败响应不带记录。
// 发送前:成功时不返回数据,失败时返回失败前的完整记录
ret = request->SetResultFlagForSuccess(0);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
ret = request->SetResultFlagForFail(3);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;

// 收到 TCAPLUS_API_PB_FIELD_SET_RES 后
int32_t result = response->GetResult();
if (result != TcapErrCode::GEN_ERR_SUC && response->GetRecordCount() > 0)
{
    const TcaplusService::TcaplusServiceRecord* record = NULL;
    if (response->FetchRecord(record) == TcapErrCode::GEN_ERR_SUC && record != NULL)
    {
        ROLESUMMARYTABLE row;
        memset(&row, 0, sizeof(row));
        int32_t version = 0;
        if (record->GetData(&row, sizeof(row), &version) == TcapErrCode::GEN_ERR_SUC)
        {
            // row 是失败前的完整记录,col1.bin 中是完整 PB;可据此决定是否重试
        }
    }
}
return result;

SetResultFlagForFail(3) 只有在服务端读到记录之后才失败时(条件不成立、版本冲突、map key 缺失、下标越界等)才带回记录;记录不存在、已过期,或请求在读取记录前就被拒绝时,失败响应不带记录。

6. 适配层错误码

本地检查(SetAllTdrPbFieldNames() 返回值):

场景返回值
request 或字段数组为空、字段路径为空,或字段路径语法错误API_ERR_PARAMETER_INVALID
descriptor 中不存在字段名或 tagAPI_ERR_FIELD_NOT_EXSIST
对 packed 数组写下标;写范围下标(如 tags[0-1]);一条路径访问两层容器元素;在非 map、非 repeated 字段上写下标;路径穿过 repeated、map 或标量字段继续访问子字段API_ERR_FIELD_TYPE_NOT_MATCH
map key 写法不符:字符串 key 未加引号且不是十六进制;整数 key 不是十进制或超出 key 类型的取值范围;key 类型不支持按 key 访问API_ERR_FIELD_TYPE_NOT_MATCH
转换成功,但路径重复或父子路径冲突SetFieldNames() 返回的 API_ERR_PARAMETER_INVALID
request 未初始化、字段数过多、完整路径过长原样返回 SetFieldNames() 的错误码

转换失败时不会给 request 留下部分字段,换用正确的字段分组重新调用即可。

发送后的结果看 response->GetResult(),批量读取还要看每次 FetchRecord() 的返回值。常见服务端错误码:

错误码含义
TXHDB_ERR_RECORD_NOT_EXIST记录不存在或已过期;FieldSet 不会自动插入记录
COMMON_ERR_ELEMENT_NOT_EXIST(537)指定的 map key 不存在,或 FieldSet 更新 map 指定 key 时增量中缺少该 key;repeated 下标越界也可能返回该错误码。该错误码是正数,不要用 ret < 0 判断失败,应判断 ret != 0
COMMON_ERR_INVALID_ARRAY_INDEXrepeated 下标越界或下标写法不合法
COMMON_ERR_CONDITION_NOT_MATCHED条件不成立
SVR_ERR_FAIL_INVALID_VERSION版本校验失败
SVR_ERR_FAIL_INVALID_OPERATIONFieldSet 不支持通过 SetOperation() 携带更新表达式
SVR_ERR_FAIL_PROTOBUF_FIELD_GET / SVR_ERR_FAIL_PROTOBUF_FIELD_UPDATEBlob 中不是合法的 PB 编码,或无法按给定路径读取、更新(如路径类型与实际数据不符)
SVR_ERR_FAIL_TO_FIND_META后端版本过旧,不支持本功能,需要升级后端(见第 2 节)

详见错误码含义和处理方法。

7. 注意事项

  1. 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
  2. Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
  3. 返回的 col1.len 是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。
  4. SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责, 适配层只解决 PB field name -> field number 的映射。
  5. 路径里的一级字段名取 TDR 表定义中的字段名,不是 C++ 结构体字段名。
  6. 字段名使用 .proto 中的原始 field name,不使用 JSON name。
  7. 头文件依赖完整 protobuf descriptor API,传入 NULL 只表示本次按数字 tag 转换; 不 include 该头文件的 TDR 用户不会被引入 protobuf 依赖。
  8. 嵌套结构体里的 Blob(如本例的 col1.bin)也支持,补齐的是嵌套结构体字段本身。

8. 其它参考文档

TDR Blob 内嵌 Protobuf 部分字段读写

[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写

[TDR Generic表][C++ SDK]更新单条数据

[TDR Generic表][C++ SDK]条件过滤和更新说明